package com.db.logging;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class AsyncFileAppender implements Appender {
  
  private static final String INI_FILE = "/async-file-appender.ini";
  private static final String BUFFER_SIZE = "buffer-size";
  private static final String LOG_FILE = "log-file";
  
  private static final String DEFAULT_BUFFER_SIZE = "128";
  private static final String DEFAULT_LOG_FILE = "log.txt";
  
  private final BlockingQueue<LogEvent> buffer;
  private PrintWriter writer;
  private final Thread dispatcher;
  
  private int bufferSize;
  private String logFile;

  private static class SingletonHolder {
    public static final AsyncFileAppender HOLDER_INSTANCE = new AsyncFileAppender();
  }

  public static AsyncFileAppender getInstance() {
    return SingletonHolder.HOLDER_INSTANCE;
  }

  private AsyncFileAppender() {
    Properties properties = new Properties();
    try {
      properties.load(getClass().getResourceAsStream(INI_FILE));
      logFile = properties.getProperty(LOG_FILE, DEFAULT_LOG_FILE);
      bufferSize = Integer.parseInt(
          properties.getProperty(BUFFER_SIZE, DEFAULT_BUFFER_SIZE));
    } catch (IOException e) {
      System.err.format("[WARNING] %s : Failed to load '%s'. Default setting will be used.%n",
          this.getClass(), INI_FILE);
      logFile = DEFAULT_LOG_FILE;
      bufferSize = Integer.parseInt(DEFAULT_BUFFER_SIZE);
    }
    buffer = new ArrayBlockingQueue<>(bufferSize);
   try {
      writer = new PrintWriter(new BufferedWriter(new FileWriter(logFile, true)), true);
      System.out.format("[INFO] %s : Output has been set to '%s'.%n",
          this.getClass(), logFile);
    } catch (IOException e) {
      writer = new PrintWriter(System.out, true);
      System.err.format("[WARNING] %s : Failed to open '%s'. Output has been set to stdout.%n",
          this.getClass(), logFile);
    }
    dispatcher = new Thread(new Dispatcher());
    dispatcher.setDaemon(true);
    dispatcher.setPriority(Thread.MIN_PRIORITY);
    dispatcher.start();
  }
  
  @Override
  public void append(LogEvent event) {
    try {
      buffer.put(event);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
  
  class Dispatcher implements Runnable {

    @Override
    public void run() {
      final SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss:SSS");
      while (true) {
        try {
          LogEvent e = buffer.take();
          String msg = sdf.format(e.getDate()) + " " + e.getLevel() + " ["
              + e.getThread() + "] " + e.getLogger() + " : "
              + e.getMessage();
          writer.println(msg);
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        } catch (Exception e) {
          System.err.format("[ERROR] %s : Dispatcher thread caught %s (%s) %n",
              this.getClass(), e, e.getMessage() != null ? e.getMessage() : "");
        }
      }
    }
  }
}
